package org.dreamwork.jasmine2.engine;

import com.google.gson.Gson;
import org.dreamwork.gson.GsonHelper;
import org.dreamwork.jasmine2.ajax.AjaxException;
import org.dreamwork.jasmine2.ajax.IAjaxData;
import org.dreamwork.jasmine2.ajax.RequestMethod;
import org.dreamwork.jasmine2.annotation.AjaxMethod;
import org.dreamwork.jasmine2.annotation.AjaxParameter;
import org.dreamwork.util.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by IntelliJ IDEA.
 * User: seth.yang
 * Date: 2010-12-9
 * Time: 23:48:02
 */
@WebServlet (urlPatterns = {"*.ajax", "/ajax/*"})
public class AjaxServlet extends HttpServlet {
    private static final Logger logger = LoggerFactory.getLogger (AjaxServlet.class);
    private static final Map<Type, IConverter> converters = new WeakHashMap<> ();

    private IConverter defaultConverter = new DefaultConverter ();
//    private Gson g = GsonHelper.getGson ();

    private static class AjaxServiceInfo {
        String methodName, serviceName;

        @Override
        public String toString () {
            return "{methodName:" + methodName + ",serviceName:" + serviceName + "}";
        }
    }

    @Override
    public void init () throws ServletException {
        try {
            AjaxManager.loadServiceList ();
        } catch (Exception ex) {
            logger.warn (ex.getMessage (), ex);
            throw new ServletException (ex);
        }
    }

    @Override
//    @SuppressWarnings ("unchecked")
    protected void service (HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String m = request.getMethod ();
        RequestMethod method = RequestMethod.parse (m);
        AjaxServiceInfo info = getName (request);
        if (logger.isTraceEnabled ()) {
            logger.trace ("method: " + method + ", serviceInfo: " + info);
        }
        if (StringUtil.isEmpty (info.serviceName)) {
            response.sendError (HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        Class c = AjaxManager.get (info.serviceName);

//        AjaxService as = (AjaxService) c.getAnnotation (AjaxService.class);
        try {
            Object o = c.newInstance ();
            Method[] methods = c.getMethods ();
            Method target = null;
            for (Method am : methods) {
                AjaxMethod aam = am.getAnnotation (AjaxMethod.class);
                if (aam == null) {
                    continue;
                }

                if (logger.isTraceEnabled ()) {
                    logger.trace ("found ajax method annotation: " + aam);
                    logger.trace ("    mapping = " + aam.mapping ());
                    logger.trace ("    pattern = " + aam.pattern ());
                }
                String mapping = aam.mapping ();
                if (!StringUtil.isEmpty (mapping) && info.methodName.equals (mapping.trim ())) {
                    if (logger.isTraceEnabled ()) {
                        logger.trace ("matched at mapping");
                    }
                    target = am;
                } else if (am.getName ().equals (info.methodName)) {
                    if (logger.isTraceEnabled ()) {
                        logger.trace ("matched at native method name");
                    }
                    target = am;
                } else if (!StringUtil.isEmpty (aam.pattern ())) {
                    Pattern pattern = Pattern.compile (aam.pattern ());
                    Matcher matcher = pattern.matcher (info.methodName);
                    if (matcher.matches ()) {
                        if (logger.isTraceEnabled ()) {
                            logger.trace ("matched at pattern");
                        }
                        target = am;
                    }
                }

                if (target != null) {
                    if (!aam.support ()) {
                        response.sendError (HttpServletResponse.SC_NOT_ACCEPTABLE, "Service \"" + info.methodName + "\" is not an AjaxService!");
                        return;
                    }

                    if (method != aam.method ()) {
                        response.sendError (HttpServletResponse.SC_METHOD_NOT_ALLOWED, "ONLY '" + method + "' IS ALLOWED!");
                        return;
                    }

                    if (logger.isDebugEnabled ()) {
                        logger.debug ("find method: " + am);
                        logger.debug ("calling the ajax service with encoding: " + aam.charset ());
                    }
                    request.setCharacterEncoding (aam.charset ());
                    execute (o, am, aam, request, response);
                    return;
                }
            }
            response.sendError (HttpServletResponse.SC_NOT_FOUND, "Service \"" + info.methodName + "\" not exists ");
        } catch (Exception e) {
            logger.warn (e.getMessage (), e);
            throw new ServletException (e);
        } finally {
            // add by seth.yang on 2013-01-08
            // fix memory leak
            HttpContext.threadLocal.remove ();
        }
    }

    private void showError (HttpServletResponse response, Throwable e) throws IOException {
        response.resetBuffer ();
        response.setContentType ("text/plain;charset=utf-8");
        Map<String, Object> map = new HashMap<> ();
        map.put ("status", "fail");
        map.put ("cause", e.getMessage ());
        if (e instanceof AjaxException) {
            map.put ("code", ((AjaxException) e).getCode ());
        }
        Gson g = GsonHelper.getGson ();
        String content = g.toJson (map);
        if (logger.isDebugEnabled ()) {
            logger.debug ("content = {}", content);
        }
        response.getWriter ().write (content);
    }

    private AjaxServiceInfo getName (HttpServletRequest request) {
        AjaxServiceInfo info = new AjaxServiceInfo ();
        String path = request.getServletPath ();
        int pos = path.lastIndexOf ('/'), p2 = path.lastIndexOf ('.');
        info.methodName = path.substring (pos + 1, p2);
        path = path.substring (0, pos);
        pos = path.lastIndexOf ('/');
        info.serviceName = path.substring (pos + 1);
        return info;
    }

    @SuppressWarnings ("unchecked")
    private void execute (Object bean, Method method, AjaxMethod am, HttpServletRequest request, HttpServletResponse response) throws Exception {
        assert am != null;
        if (am.noCache ()) { // no cache
            response.setHeader("Pragma","No-cache");
            response.setHeader("Cache-Control","no-cache");
            response.setDateHeader("Expires", 0);
        }

        // build HttpContext
        HttpContext context = new HttpContext ();
        context.setApplication (getServletContext ());
        context.setRequest (request);
        context.setResponse (response);
        context.setSession (request.getSession ());

        try {
            // parse method and translate parameter(s)
            Annotation[][] anns = method.getParameterAnnotations ();
            Class[] types = method.getParameterTypes ();
            Object[] values = new Object[types.length];
            if (logger.isTraceEnabled ()) {
                logger.trace ("this service needs " + types.length + " parameters:");
            }
            for (int i = 0; i < anns.length; i++) {
                AjaxParameter ap = (AjaxParameter) anns[i][0];
                String value = request.getParameter (ap.name ());
                if (logger.isTraceEnabled ()) {
                    logger.trace (String.format ("process parameter[%d], name = %s, type = %s, value=%s",
                            i, ap.name (), types [i], value));
                }
                if (StringUtil.isEmpty (value)) {
                    values[i] = null;
                } else {
                    if (CharSequence.class.isAssignableFrom (types[i])) {
                        values[i] = value;
                    } else {
                        if (logger.isTraceEnabled ()) {
                            logger.trace ("parameter[" + ap.name () + "] is not a string, trying to find related converter.");
                        }
                        Class<? extends IConverter> cc = ap.converter ();
                        IConverter converter = findConverter (types[i], cc);
                        values[i] = converter.cast (types[i], value);
                    }
                }
            }
            Object ret = method.invoke (bean, values);
            String contentType = am.contentType ();
            response.setContentType (contentType);

            if (am.wrapResult ()) {
                showJsonData (response, ret);
            } else {
                if (ret == null) {
                    return;
                }

                if (ret instanceof CharSequence) {
                    Writer writer = response.getWriter ();
                    writer.write (ret.toString ());
                    writer.flush ();
//                    return;
                } else if (ret instanceof IAjaxData) {
                    OutputStream out = response.getOutputStream ();
                    out.write (((IAjaxData) ret).toByteArray ());
                    out.flush ();
                } else {
//                if (contentType.startsWith ("text/")) {
                    Gson g = GsonHelper.getGson ();
                    Writer writer = response.getWriter ();
                    writer.write (g.toJson (ret));
                    writer.flush ();
                }
            }
        } catch (InvocationTargetException ex) {
            logger.warn (ex.getMessage (), ex);
            showError (response, ex.getTargetException ());
        } catch (Exception ex) {
            logger.warn (ex.getMessage (), ex);
            showError (response, ex);
        }
    }

    private void showJsonData (HttpServletResponse response, Object data) throws IOException {
        Map<String, Object> map = new HashMap<> ();
        map.put ("status", "ok");
        if (data != null) {
            if (data instanceof CharSequence) {
                String text = data.toString ();
                if (!StringUtil.isEmpty (text)) {
                    map.put ("data", text.trim ());
                }
            } else {
                map.put ("data", data);
            }
        }

        Gson g = GsonHelper.getGson ();
        String content = g.toJson (map);
        if (logger.isTraceEnabled ()) {
            logger.trace ("response: " + content);
        }
        response.getWriter ().write (content);
    }

    private synchronized IConverter findConverter (Type type, Class<? extends IConverter> typeOfConverter) {
        if (typeOfConverter == DefaultConverter.class) {
            if (logger.isTraceEnabled ()) {
                logger.trace ("use the default converter");
            }
            return defaultConverter;
        }

        if (converters.containsKey (type)) {
            IConverter converter = converters.get (type);
            if (logger.isTraceEnabled ()) {
                logger.trace ("using cached converter: " + converter);
            }
            return converter;
        }

        try {
            if (logger.isTraceEnabled ()) {
                logger.trace ("there's no converter of type: " + typeOfConverter + ", trying to create new one.");
            }
            IConverter converter = typeOfConverter.newInstance ();
            converters.put (type, converter);
            return converter;
        } catch (Exception ex) {
            logger.warn (ex.getMessage (), ex);
        }
        return defaultConverter;
    }
}